/**
 * ***************************************************************************** Copyright (c) 2000,
 * 2011 IBM Corporation and others. All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0 which accompanies this
 * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * <p>Contributors: IBM Corporation - initial API and implementation
 * *****************************************************************************
 */
package org.eclipse.jdt.internal.corext.refactoring.rename;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeParameter;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.refactoring.IJavaRefactorings;
import org.eclipse.jdt.core.refactoring.descriptors.RenameJavaElementDescriptor;
import org.eclipse.jdt.internal.core.refactoring.descriptors.RefactoringSignatureDescriptorFactory;
import org.eclipse.jdt.internal.corext.SourceRangeFactory;
import org.eclipse.jdt.internal.corext.dom.HierarchicalASTVisitor;
import org.eclipse.jdt.internal.corext.refactoring.Checks;
import org.eclipse.jdt.internal.corext.refactoring.JDTRefactoringDescriptorComment;
import org.eclipse.jdt.internal.corext.refactoring.JavaRefactoringArguments;
import org.eclipse.jdt.internal.corext.refactoring.JavaRefactoringDescriptorUtil;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringAvailabilityTester;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages;
import org.eclipse.jdt.internal.corext.refactoring.base.JavaStatusContext;
import org.eclipse.jdt.internal.corext.refactoring.changes.DynamicValidationRefactoringChange;
import org.eclipse.jdt.internal.corext.refactoring.participants.JavaProcessors;
import org.eclipse.jdt.internal.corext.refactoring.structure.ASTNodeSearchUtil;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.refactoring.tagging.IReferenceUpdating;
import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser;
import org.eclipse.jdt.internal.corext.refactoring.util.ResourceUtil;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.viewsupport.BasicElementLabels;
import org.eclipse.jdt.ui.JavaElementLabels;
import org.eclipse.jdt.ui.refactoring.RefactoringSaveHelper;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
import org.eclipse.ltk.core.refactoring.participants.RenameArguments;

/** Rename processor to rename type parameters. */
public class RenameTypeParameterProcessor extends JavaRenameProcessor
    implements IReferenceUpdating {

  /** AST visitor which searches for occurrences of the type parameter. */
  private class RenameTypeParameterVisitor extends HierarchicalASTVisitor {

    /** The binding of the type parameter */
    private final IBinding fBinding;

    /** The node of the type parameter name */
    private final SimpleName fName;

    /** The compilation unit rewrite to use */
    private final CompilationUnitRewrite fRewrite;

    /** The status of the visiting process */
    private final RefactoringStatus fStatus;

    /**
     * Creates a new rename type parameter visitor.
     *
     * @param rewrite the compilation unit rewrite to use
     * @param range the source range of the type parameter
     * @param status the status to update
     */
    public RenameTypeParameterVisitor(
        CompilationUnitRewrite rewrite, ISourceRange range, RefactoringStatus status) {
      Assert.isNotNull(rewrite);
      Assert.isNotNull(range);
      Assert.isNotNull(status);
      fRewrite = rewrite;
      fName = (SimpleName) NodeFinder.perform(rewrite.getRoot(), range);
      fBinding = fName.resolveBinding();
      fStatus = status;
    }

    /**
     * Returns the resulting change.
     *
     * @return the resulting change
     * @throws CoreException if the change could not be created
     */
    public Change getResult() throws CoreException {
      return fRewrite.createChange(true);
    }

    @Override
    public boolean visit(SimpleName node) {
      IBinding binding = node.resolveBinding();
      if (fBinding == binding) {
        String groupDescription = null;
        if (node != fName) {
          if (fUpdateReferences) {
            groupDescription =
                RefactoringCoreMessages
                    .RenameTypeParameterRefactoring_update_type_parameter_reference;
          }
        } else {
          groupDescription =
              RefactoringCoreMessages
                  .RenameTypeParameterRefactoring_update_type_parameter_declaration;
        }
        if (groupDescription != null) {
          fRewrite
              .getASTRewrite()
              .set(
                  node,
                  SimpleName.IDENTIFIER_PROPERTY,
                  getNewElementName(),
                  fRewrite.createGroupDescription(groupDescription));
        }
      }
      return true;
    }

    @Override
    public boolean visit(AbstractTypeDeclaration node) {
      String name = node.getName().getIdentifier();
      if (name.equals(getNewElementName())) {
        fStatus.addError(
            Messages.format(
                RefactoringCoreMessages
                    .RenameTypeParameterRefactoring_type_parameter_inner_class_clash,
                new String[] {name}),
            JavaStatusContext.create(
                fTypeParameter.getDeclaringMember().getCompilationUnit(),
                SourceRangeFactory.create(node)));
        return true;
      }
      return true;
    }
  }

  private static final String ATTRIBUTE_PARAMETER = "parameter"; // $NON-NLS-1$

  /** The identifier of this processor */
  public static final String IDENTIFIER =
      "org.eclipse.jdt.ui.renameTypeParameterProcessor"; // $NON-NLS-1$

  /** The change object */
  private Change fChange = null;

  /** The type parameter to rename */
  private ITypeParameter fTypeParameter;

  /** Should references to the type parameter be updated? */
  private boolean fUpdateReferences = true;

  /**
   * Creates a new rename type parameter processor.
   *
   * @param parameter the type parameter to rename, or <code>null</code> if invoked by scripting
   */
  public RenameTypeParameterProcessor(ITypeParameter parameter) {
    fTypeParameter = parameter;
    if (parameter != null) setNewElementName(parameter.getElementName());
  }

  public RenameTypeParameterProcessor(
      JavaRefactoringArguments arguments, RefactoringStatus status) {
    this(null);
    status.merge(initialize(arguments));
  }

  @Override
  protected RenameModifications computeRenameModifications() throws CoreException {
    RenameModifications result = new RenameModifications();
    result.rename(fTypeParameter, new RenameArguments(getNewElementName(), getUpdateReferences()));
    return result;
  }

  @Override
  protected IFile[] getChangedFiles() throws CoreException {
    return new IFile[] {
      ResourceUtil.getFile(fTypeParameter.getDeclaringMember().getCompilationUnit())
    };
  }

  @Override
  public int getSaveMode() {
    return RefactoringSaveHelper.SAVE_NOTHING;
  }

  @Override
  protected RefactoringStatus doCheckFinalConditions(
      IProgressMonitor monitor, CheckConditionsContext context)
      throws CoreException, OperationCanceledException {
    Assert.isNotNull(monitor);
    Assert.isNotNull(context);
    RefactoringStatus status = new RefactoringStatus();
    try {
      monitor.beginTask("", 5); // $NON-NLS-1$
      monitor.setTaskName(RefactoringCoreMessages.RenameTypeParameterRefactoring_checking);
      status.merge(Checks.checkIfCuBroken(fTypeParameter.getDeclaringMember()));
      monitor.worked(1);
      if (!status.hasFatalError()) {
        status.merge(checkNewElementName(getNewElementName()));
        monitor.worked(1);
        monitor.setTaskName(RefactoringCoreMessages.RenameTypeParameterRefactoring_searching);
        status.merge(createRenameChanges(new SubProgressMonitor(monitor, 2)));
        monitor.setTaskName(RefactoringCoreMessages.RenameTypeParameterRefactoring_checking);
        if (status.hasFatalError()) return status;
        monitor.worked(1);
      }
    } finally {
      monitor.done();
    }
    return status;
  }

  @Override
  public RefactoringStatus checkInitialConditions(IProgressMonitor monitor)
      throws CoreException, OperationCanceledException {
    Assert.isNotNull(monitor);
    if (!fTypeParameter.exists())
      return RefactoringStatus.createFatalErrorStatus(
          Messages.format(
              RefactoringCoreMessages.RenameTypeParameterRefactoring_deleted,
              BasicElementLabels.getFileName(
                  fTypeParameter.getDeclaringMember().getCompilationUnit())));
    return Checks.checkIfCuBroken(fTypeParameter.getDeclaringMember());
  }

  public RefactoringStatus checkNewElementName(String name) throws CoreException {
    Assert.isNotNull(name);
    RefactoringStatus result = Checks.checkTypeParameterName(name, fTypeParameter);
    if (Checks.startsWithLowerCase(name))
      result.addWarning(
          RefactoringCoreMessages.RenameTypeParameterRefactoring_should_start_lowercase);
    if (Checks.isAlreadyNamed(fTypeParameter, name))
      result.addFatalError(RefactoringCoreMessages.RenameTypeParameterRefactoring_another_name);

    IMember member = fTypeParameter.getDeclaringMember();
    if (member instanceof IType) {
      IType type = (IType) member;
      if (type.getTypeParameter(name).exists())
        result.addFatalError(
            RefactoringCoreMessages
                .RenameTypeParameterRefactoring_class_type_parameter_already_defined);
    } else if (member instanceof IMethod) {
      IMethod method = (IMethod) member;
      if (method.getTypeParameter(name).exists())
        result.addFatalError(
            RefactoringCoreMessages
                .RenameTypeParameterRefactoring_method_type_parameter_already_defined);
    } else {
      JavaPlugin.logErrorMessage(
          "Unexpected sub-type of IMember: " + member.getClass().getName()); // $NON-NLS-1$
      Assert.isTrue(false);
    }
    return result;
  }

  @Override
  public Change createChange(IProgressMonitor monitor)
      throws CoreException, OperationCanceledException {
    Assert.isNotNull(monitor);
    try {
      Change change = fChange;
      if (change != null) {
        String project = null;
        IJavaProject javaProject = fTypeParameter.getJavaProject();
        if (javaProject != null) project = javaProject.getElementName();
        String description =
            Messages.format(
                RefactoringCoreMessages.RenameTypeParameterProcessor_descriptor_description_short,
                BasicElementLabels.getJavaElementName(fTypeParameter.getElementName()));
        String header =
            Messages.format(
                RefactoringCoreMessages.RenameTypeParameterProcessor_descriptor_description,
                new String[] {
                  BasicElementLabels.getJavaElementName(fTypeParameter.getElementName()),
                  JavaElementLabels.getElementLabel(
                      fTypeParameter.getDeclaringMember(), JavaElementLabels.ALL_FULLY_QUALIFIED),
                  BasicElementLabels.getJavaElementName(getNewElementName())
                });
        String comment = new JDTRefactoringDescriptorComment(project, this, header).asString();
        RenameJavaElementDescriptor descriptor =
            RefactoringSignatureDescriptorFactory.createRenameJavaElementDescriptor(
                IJavaRefactorings.RENAME_TYPE_PARAMETER);
        descriptor.setProject(project);
        descriptor.setDescription(description);
        descriptor.setComment(comment);
        descriptor.setFlags(RefactoringDescriptor.NONE);
        descriptor.setJavaElement(fTypeParameter);
        descriptor.setNewName(getNewElementName());
        descriptor.setUpdateReferences(fUpdateReferences);
        change =
            new DynamicValidationRefactoringChange(
                descriptor,
                RefactoringCoreMessages.RenameTypeParameterProcessor_change_name,
                new Change[] {change});
      }
      return change;
    } finally {
      fChange = null;
      monitor.done();
    }
  }

  /**
   * Creates the necessary changes for the renaming of the type parameter.
   *
   * @param monitor the progress monitor to display progress
   * @return the status of the operation
   * @throws CoreException if the change could not be generated
   */
  private RefactoringStatus createRenameChanges(IProgressMonitor monitor) throws CoreException {
    Assert.isNotNull(monitor);
    RefactoringStatus status = new RefactoringStatus();
    try {
      monitor.beginTask(RefactoringCoreMessages.RenameTypeParameterRefactoring_searching, 2);
      ICompilationUnit cu = fTypeParameter.getDeclaringMember().getCompilationUnit();
      CompilationUnit root = RefactoringASTParser.parseWithASTProvider(cu, true, null);
      CompilationUnitRewrite rewrite = new CompilationUnitRewrite(cu, root);
      IMember member = fTypeParameter.getDeclaringMember();
      ASTNode declaration = null;
      if (member instanceof IMethod) {
        declaration = ASTNodeSearchUtil.getMethodDeclarationNode((IMethod) member, root);
      } else if (member instanceof IType) {
        declaration = ASTNodeSearchUtil.getAbstractTypeDeclarationNode((IType) member, root);
      } else {
        JavaPlugin.logErrorMessage(
            "Unexpected sub-type of IMember: " + member.getClass().getName()); // $NON-NLS-1$
        Assert.isTrue(false);
      }
      monitor.worked(1);
      RenameTypeParameterVisitor visitor =
          new RenameTypeParameterVisitor(rewrite, fTypeParameter.getNameRange(), status);
      if (declaration != null) declaration.accept(visitor);
      fChange = visitor.getResult();
    } finally {
      monitor.done();
    }
    return status;
  }

  @Override
  protected String[] getAffectedProjectNatures() throws CoreException {
    return JavaProcessors.computeAffectedNatures(fTypeParameter);
  }

  public String getCurrentElementName() {
    return fTypeParameter.getElementName();
  }

  @Override
  public Object[] getElements() {
    return new Object[] {fTypeParameter};
  }

  @Override
  public String getIdentifier() {
    return IDENTIFIER;
  }

  public Object getNewElement() throws CoreException {
    IMember member = fTypeParameter.getDeclaringMember();
    if (member instanceof IType) {
      IType type = (IType) member;
      return type.getTypeParameter(getNewElementName());
    } else if (member instanceof IMethod) {
      IMethod method = (IMethod) member;
      return method.getTypeParameter(getNewElementName());
    } else {
      JavaPlugin.logErrorMessage(
          "Unexpected sub-type of IMember: " + member.getClass().getName()); // $NON-NLS-1$
      Assert.isTrue(false);
    }
    return null;
  }

  @Override
  public String getProcessorName() {
    return RefactoringCoreMessages.RenameTypeParameterProcessor_name;
  }

  public boolean getUpdateReferences() {
    return fUpdateReferences;
  }

  private RefactoringStatus initialize(JavaRefactoringArguments extended) {
    String parameter = extended.getAttribute(ATTRIBUTE_PARAMETER);
    if (parameter == null || "".equals(parameter)) // $NON-NLS-1$
    return RefactoringStatus.createFatalErrorStatus(
          Messages.format(
              RefactoringCoreMessages.InitializableRefactoring_argument_not_exist,
              ATTRIBUTE_PARAMETER));
    String handle = extended.getAttribute(JavaRefactoringDescriptorUtil.ATTRIBUTE_INPUT);
    if (handle != null) {
      IJavaElement element =
          JavaRefactoringDescriptorUtil.handleToElement(extended.getProject(), handle, false);
      if (element == null || !element.exists())
        return JavaRefactoringDescriptorUtil.createInputFatalStatus(
            element, getProcessorName(), IJavaRefactorings.RENAME_TYPE_PARAMETER);
      else {
        if (element instanceof IMethod)
          fTypeParameter = ((IMethod) element).getTypeParameter(parameter);
        else if (element instanceof IType)
          fTypeParameter = ((IType) element).getTypeParameter(parameter);
        else
          return RefactoringStatus.createFatalErrorStatus(
              Messages.format(
                  RefactoringCoreMessages.InitializableRefactoring_illegal_argument,
                  new Object[] {handle, JavaRefactoringDescriptorUtil.ATTRIBUTE_INPUT}));
        if (fTypeParameter == null || !fTypeParameter.exists())
          return JavaRefactoringDescriptorUtil.createInputFatalStatus(
              fTypeParameter, getProcessorName(), IJavaRefactorings.RENAME_TYPE_PARAMETER);
      }
    } else
      return RefactoringStatus.createFatalErrorStatus(
          Messages.format(
              RefactoringCoreMessages.InitializableRefactoring_argument_not_exist,
              JavaRefactoringDescriptorUtil.ATTRIBUTE_INPUT));
    String name = extended.getAttribute(JavaRefactoringDescriptorUtil.ATTRIBUTE_NAME);
    if (name != null && !"".equals(name)) // $NON-NLS-1$
    setNewElementName(name);
    else
      return RefactoringStatus.createFatalErrorStatus(
          Messages.format(
              RefactoringCoreMessages.InitializableRefactoring_argument_not_exist,
              JavaRefactoringDescriptorUtil.ATTRIBUTE_NAME));
    String references = extended.getAttribute(JavaRefactoringDescriptorUtil.ATTRIBUTE_REFERENCES);
    if (references != null) {
      fUpdateReferences = Boolean.valueOf(references).booleanValue();
    } else
      return RefactoringStatus.createFatalErrorStatus(
          Messages.format(
              RefactoringCoreMessages.InitializableRefactoring_argument_not_exist,
              JavaRefactoringDescriptorUtil.ATTRIBUTE_REFERENCES));
    return new RefactoringStatus();
  }

  @Override
  public boolean isApplicable() throws CoreException {
    return RefactoringAvailabilityTester.isRenameAvailable(fTypeParameter);
  }

  public void setUpdateReferences(boolean update) {
    fUpdateReferences = update;
  }
}
